import type { ComputedRef, Ref } from 'vue';
import { computed, ref } from 'vue';
import { v4 as uuid } from 'uuid';
import type { ChatMessage } from '@n8n/chat/types';
import type {
	ITaskData,
	INodeExecutionData,
	IBinaryKeyData,
	IDataObject,
	IRunExecutionData,
} from 'n8n-workflow';
import { useToast } from '@/app/composables/useToast';
import { useMessage } from '@/app/composables/useMessage';
import { usePinnedData } from '@/app/composables/usePinnedData';
import { MODAL_CONFIRM } from '@/app/constants';
import { useI18n } from '@n8n/i18n';
import type { INodeUi } from '@/Interface';
import type { IExecutionPushResponse } from '@/features/execution/executions/executions.types';
import {
	extractBotResponse,
	getInputKey,
	processFiles,
} from '@/features/execution/logs/logs.utils';
import { convertFileToBinaryData } from '@/app/utils/fileUtils';

export type RunWorkflowChatPayload = {
	triggerNode: string;
	nodeData: ITaskData;
	source: string;
	message: string;
};
export interface ChatMessagingDependencies {
	chatTrigger: Ref<INodeUi | null>;
	sessionId: Ref<string> | ComputedRef<string>;
	executionResultData: ComputedRef<IRunExecutionData['resultData'] | undefined>;
	onRunChatWorkflow: (
		payload: RunWorkflowChatPayload,
	) => Promise<IExecutionPushResponse | undefined>;
	ws: Ref<WebSocket | null>;
	onNewMessage: (message: ChatMessage) => void;
}

export function useChatMessaging({
	chatTrigger,
	sessionId,
	executionResultData,
	onRunChatWorkflow,
	ws,
	onNewMessage,
}: ChatMessagingDependencies) {
	const locale = useI18n();
	const { showError } = useToast();
	const previousMessageIndex = ref(0);
	const isLoading = ref(false);

	const setLoadingState = (loading: boolean) => {
		isLoading.value = loading;
	};

	/** Gets keyed files for the workflow input */
	async function getKeyedFiles(files: File[]): Promise<IBinaryKeyData> {
		const binaryData: IBinaryKeyData = {};

		await Promise.all(
			files.map(async (file, index) => {
				const data = await convertFileToBinaryData(file);
				const key = `data${index}`;

				binaryData[key] = data;
			}),
		);

		return binaryData;
	}

	/** Extracts file metadata */
	function extractFileMeta(file: File): IDataObject {
		return {
			fileName: file.name,
			fileSize: `${file.size} bytes`,
			fileExtension: file.name.split('.').pop() ?? '',
			fileType: file.type.split('/')[0],
			mimeType: file.type,
		};
	}

	/** Starts workflow execution with the message */
	async function startWorkflowWithMessage(message: string, files?: File[]): Promise<void> {
		const triggerNode = chatTrigger.value;

		if (!triggerNode) {
			showError(new Error('Chat Trigger Node could not be found!'), 'Trigger Node not found');
			return;
		}

		const inputKey = getInputKey(triggerNode);

		const inputPayload: INodeExecutionData = {
			json: {
				sessionId: sessionId.value,
				action: 'sendMessage',
				[inputKey]: message,
			},
		};

		if (files && files.length > 0) {
			const filesMeta = files.map((file) => extractFileMeta(file));
			const binaryData = await getKeyedFiles(files);

			inputPayload.json.files = filesMeta;
			inputPayload.binary = binaryData;
		}
		const nodeData: ITaskData = {
			startTime: Date.now(),
			executionTime: 0,
			executionIndex: 0,
			executionStatus: 'success',
			data: {
				main: [[inputPayload]],
			},
			source: [null],
		};
		isLoading.value = true;
		const response = await onRunChatWorkflow({
			triggerNode: triggerNode.name,
			nodeData,
			source: 'RunData.ManualChatMessage',
			message,
		});
		isLoading.value = false;
		ws.value = null;
		if (!response?.executionId) {
			return;
		}

		// Response Node mode should not return last node result if responseMode is "responseNodes"
		const responseMode = (triggerNode.parameters.options as { responseMode?: string })
			?.responseMode;
		if (responseMode === 'responseNodes') return;

		const chatMessage = executionResultData.value
			? extractBotResponse(
					executionResultData.value,
					response.executionId,
					locale.baseText('chat.window.chat.response.empty'),
				)
			: undefined;

		if (chatMessage !== undefined) {
			onNewMessage(chatMessage);
		}
	}

	/** Sends a message to the chat */
	async function sendMessage(message: string, files?: File[]) {
		previousMessageIndex.value = 0;
		if (message.trim() === '' && (!files || files.length === 0)) {
			showError(
				new Error(locale.baseText('chat.window.chat.provideMessage')),
				locale.baseText('chat.window.chat.emptyChatMessage'),
			);
			return;
		}

		const pinnedChatData = usePinnedData(chatTrigger.value);
		if (pinnedChatData.hasData.value) {
			const confirmResult = await useMessage().confirm(
				locale.baseText('chat.window.chat.unpinAndExecute.description'),
				locale.baseText('chat.window.chat.unpinAndExecute.title'),
				{
					confirmButtonText: locale.baseText('chat.window.chat.unpinAndExecute.confirm'),
					cancelButtonText: locale.baseText('chat.window.chat.unpinAndExecute.cancel'),
				},
			);

			if (!(confirmResult === MODAL_CONFIRM)) return;

			pinnedChatData.unsetData('unpin-and-send-chat-message-modal');
		}

		const newMessage: ChatMessage & { sessionId: string } = {
			text: message,
			sender: 'user',
			sessionId: sessionId.value,
			id: uuid(),
			files,
		};
		onNewMessage(newMessage);

		if (ws.value?.readyState === WebSocket.OPEN && !isLoading.value) {
			ws.value.send(
				JSON.stringify({
					sessionId: sessionId.value,
					action: 'sendMessage',
					chatInput: message,
					files: await processFiles(files),
				}),
			);
			isLoading.value = true;
		} else {
			await startWorkflowWithMessage(newMessage.text, files);
		}
	}

	return {
		previousMessageIndex,
		isLoading: computed(() => isLoading.value),
		setLoadingState,
		sendMessage,
	};
}
